Redux Toolkit Query (RTK Query)
RTK Query는 “서버에서 데이터 가져오고(조회·수정) 자동으로 캐시하고 동기화해주는 도구”입니다.
즉, fetch/axios + createAsyncThunk + 여러 slice로 하던 귀찮은 반복을 훅과 선언만으로 해결해줍니다.
1️⃣ 장점 (단순함)
- API를 하나의 모듈(apiSlice)에 선언
- 선언한 엔드포인트마다 자동으로 React 훅이 생성 (
useGetTodosQuery) - 훅을 쓰면 데이터, 로딩, 에러 상태가 자동으로 제공
- 같은 쿼리를 여러 컴포넌트에서 쓰면 캐시를 공유해서 중복 요청을 막음
- 데이터 변경(POST/PUT/DELETE)은 태그(tag) 를 통해 관련 쿼리를 자동을 새로고침(무효화)
2️⃣ 기본 개념 요약
- API slice:
createApi로 만든 모듈. 엔드포인트들을 모아둠 - query:GET 계열, 캐시 가능한 조회
- mutation: POST/PUT/DELETE 같은 변경 요청, 캐시 무효화 트리거를 통해 관련 쿼리 갱신
- tags: 리소스 라벨 - 어떤 mutation이 어떤 query를 invalildates/provides 하는지 연결
- fetchBaseQuery: 내부적으로
fetch를 래핑한 기본 baseQuery. 간단한 API엔 이걸 사용 - munual refetch / laze query: 필요 시 실행하는 쿼리 (자동 실행이 아니라 훅 호출 제어)
- optimistic updates: 서버 응답을 기다리지 않고 클라이언트 상태를 먼저 업데이트
3️⃣ 기본 사용 예제 - createApi와 엔드포인트
📄 apiSlice.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const api = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({
baseUrl: "/api",
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token;
if (token) headers.set("authorization", `Bearer ${token}`);
return headers;
},
}),
tagTypes: ["User", "Todo"],
endpoints: (builder) => ({
// 사용자 조회
getUser: builder.query({
query: (id) => `/user/${id}`,
providesTags: (result, error, id) => [{ type: "User", id }],
}),
// Todo 조회
getTodos: builder.query({
query: () => "/todos",
providesTags: (result) =>
result
? [
...result.map((r) => ({ type: "Todo", id: r.id })),
{ type: "Todo", id: "LIST" },
]
: [{ type: "Todo", id: "LIST" }],
}),
// Todo 추가
addTodo: builder.mutation({
query: (newTodo) => ({
url: "/todos",
method: "POST",
body: newTodo,
}),
invalidatesTags: [{ type: "Todo", id: "LIST" }],
}),
// Todo done 토글
toggleTodo: builder.mutation({
query: ({ id, done }) => ({
url: `/todos/${id}`,
method: "PATCH",
body: { done },
}),
invalidatesTags: (result, error, { id }) => [{ type: "Todo", id }],
}),
}),
});
export const {
useGetUserQuery,
useGetTodosQuery,
useAddTodoMutation,
useToggleTodoMutation,
} = api;
createApi
- API를 정의하는 핵심 함수
- reducer + middleware 자동 생성
baseQuery
- 모든 fetch 요청의 공통 설정
- 예: baseUrl, 헤더, 토큰 설정 등
endpoints
- GET, POST, PATH 등 실제 API 요청 정의
query→ GETmutation→ POST / PATCH / DELETE 등
Tag 기반 캐시 관리
providesTags→ 데이터가 제공하는 태그 (캐시를 저장할 때 붙이는 라벨)invalidatesTags→ 이 태그를 가진 캐시를 무효화 → 자동 재요청 (데이터가 변화되어 다시 요청하도록 하는 신호)
Auto Hooks 생성
useGetTodosQueryuseAddTodoMutation- 자동 생성되는게 RTK Query의 큰 장점
📄 store 설정
import { configureStore } from "@reduxjs/toolkit";
import { api } from "./apiSlice";
import authReducer from "./authSlice";
const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
auth: authReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
});
export default store;
4️⃣ React에서 훅 사용 예시
function TodoList() {
const { data: todos, error, isLoading, refetch } = useGetTodosQuery();
const [addTodo] = useAddTodoMutation();
if (isLoading) return <div>로딩중…</div>;
if (error) return <div>에러: {error.toString()}</div>;
return (
<div>
<button onClick={() => refetch()}>수동 새로고침</button>
<ul>
{todos.map((t) => (
<li key={t.id}>
{t.text}
<button onClick={() => addTodo({ text: "새 할일" })}>추가</button>
</li>
))}
</ul>
</div>
);
}
useGet{}Query는 mount 시 자동 요청(자동 캐싱)refetch()로 수동 재요청 가능use{}}Mutation은[triggerFn, {status...}]형태도 가능 (const [addTodo, result] = useAddTopdoMutation())
5️⃣ Tag 기반 캐시 무효화 (provides/invalidates)
- Query에서
providesTags로 어떤 태그르 제공(provide)할지 선언 - Mutationdㅔ서
invalidatesTags로 어떤 태그를 무효화(invalidate)할지 선언 - 결과: 해당 태그를 제공하던 쿼리가 자동 리패치되어 최신 데이터 유지
🔹 예시 패턴
- 리스트 조회 →
provides: [{ type: 'Todo', id: 'LIST' }] - 항목 생성/삭제 →
invalidates: [{ type: 'Todo', id: 'LIST' }] - 항목 업데이트 →
invalidates: [{ typeL 'Todo', id: 'itemId' }]
이 패턴을 일관되게 적용하면 수동 상태 조작 없이도 서버-클라이언트 상태를 동기화할 수 있습니다.
6️⃣ Optimistic Update (낙관적 업데이터)
서버 응답을 기다리지 않고 UI를 즉시 업데이트하려면 updateQueryData와 path를 사용해야합니다.
🧐 예시: 체크박스 토글 낙관 업데이트
// 컴포넌트 내부
const [toggleTodo] = useToggleTodoMutation();
const patchResult = api.util.updateQueryData('getTodos', undefined, (draft) => {
const todo = draft.find(t => t.id === id);
if (todo) todo.done = !todo.done;
});
try {
const res = await toggleTodo({ id, done: !currentDone }).unwrap();
// 성공 시 아무 작업 필요 없음 (서버 응답에 따라 캐시가 이미 갱신될 수도)
} catch (err) {
patchResult.undo(); // 실패하면 롤백
}
unwarp()을 쓰면 rejection을 try/catch로 잡아 처리할 수 있음pathchResult.undo()를 호출하면 낙관 업데이트를 롤백 가능- 여러 쿼리를 동시에 수정해야 하면
dispatch(api.util.undateQueryDate(...))패턴을 사용
7️⃣ Lazy Query, Polling, Re-fetch 전략
- Lazy queries:
useLazyQuery혹은 사용하면 필요할 때만 쿼리를 실행
const [trigger, result] = useLazyGetUserQuery();
// trigger(userId) 로 실행
- Polling: 자동으로 주기적 재요청. 쿼리 옵션에서
pollingInterval사용
useGetTodosQuery(undefined, { pollingInterval: 10000 }); // 10초마다 갱신
- Refetch on Focus / Reconnect:
setupListeners와 함께refetchOnFocus: true,refetchOnReconnect: true를 사용하면 브라우저 포커스나 네트워크 복구 시 자동 재요청
import { setupListeners } from "@reduxjs/toolkit/query";
setupListeners(store.dispatch);
8️⃣ Error 처리 및 rejectWithValue 유사 패턴
fetchBaseQuery는error포맷을 통일해서 반환.- 훅 결과에서
isError,error속성 사용. - 뮤테이션에서
unwrap()으로 성공/실패를 프로미스 스타일로 다룰 수 있음. - 전역적인 에러 처리는
baseQuery레벨에서 처리 (예: 401 -> 리프레시 토큰 시도).
🔹 baseQuery에서 토큰 만료 처리 예시
const baseQuery = fetchBaseQuery({ baseUrl: '/api', prepareHeaders });
const baseQueryWithReauth = async (args, api, extraOptions) => {
let result = await baseQuery(args, api, extraOptions);
if (result.error && result.error.status === 401) {
// 토큰 갱신 로직 실행 (예시)
const refreshResult = await baseQuery('/refresh', api, extraOptions);
if (refreshResult.data) {
api.dispatch(setCredentials(refreshResult.data));
// 원래 요청 재시도
result = await baseQuery(args, api, extraOptions);
} else {
// 로그아웃 처리 등
}
}
return result;
};
export const api = createApi({ baseQuery: baseQueryWithReauth, ... });